#!/usr/bin/env bash
#
# Copyright (c) 2017-2020 Intel Corporation
#
# SPDX-License-Identifier: Apache-2.0
#

# kata-deploy installs binaries in /opt/kata/bin by default, which is not in the PATH.
# We need to add it to PATH to ensure commands like kata-monitor and kata-runtime.
SCRIPT_DIR="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")")"

typeset -r script_name=${0##*/}
typeset -r runtime_name="@RUNTIME_NAME@"
typeset -r runtime_path=$(PATH="$SCRIPT_DIR:$PATH" command -v "$runtime_name" 2>/dev/null)
typeset -r runtime_snap_name="kata-containers.runtime"
typeset -r runtime_snap_path=$(command -v "$runtime_snap_name" 2>/dev/null)
typeset -r runtime=${runtime_path:-"$runtime_snap_path"}

typeset -r containerd_shim_v2_name="containerd-shim-kata-v2"
typeset -r containerd_shim_v2=$(PATH="$SCRIPT_DIR:$PATH" command -v "$containerd_shim_v2_name" 2>/dev/null)

typeset -r kata_monitor_name="kata-monitor"
typeset -r kata_monitor=$(PATH="$SCRIPT_DIR:$PATH" command -v "$kata_monitor_name" 2>/dev/null)

typeset -r issue_url="@PROJECT_BUG_URL@"
typeset -r script_version="@VERSION@ (commit @COMMIT@)"

typeset -r unknown="unknown"

typeset -r osbuilder_file="/var/lib/osbuilder/osbuilder.yaml"

# Maximum number of errors to show for a single system component
# (such as runtime).
PROBLEM_LIMIT=${PROBLEM_LIMIT:-50}

# List of patterns used to detect problems in logfiles.
problem_pattern="("
problem_pattern+="\<abort|"
problem_pattern+="\<bug\>|"
problem_pattern+="\<cannot\>|"
problem_pattern+="\<catastrophic|"
problem_pattern+="\<could not\>|"
problem_pattern+="\<couldn\'t\>|"
problem_pattern+="\<critical|"
problem_pattern+="\<die\>|"
problem_pattern+="\<died\>|"
problem_pattern+="\<does.*not.*exist\>|"
problem_pattern+="\<dying\>|"
problem_pattern+="\<empty\>|"
problem_pattern+="\<erroneous|"
problem_pattern+="\<error|"
problem_pattern+="\<expected\>|"
problem_pattern+="\<fail|"
problem_pattern+="\<fatal|"
problem_pattern+="\<impossible\>|"
problem_pattern+="\<impossibly\>|"
problem_pattern+="\<incorrect|"
problem_pattern+="\<invalid\>|"
problem_pattern+="\<level=\"*error\"* |"
problem_pattern+="\<level=\"*fatal\"* |"
problem_pattern+="\<level=\"*panic\"* |"
problem_pattern+="\<level=\"*warning\"* |"
problem_pattern+="\<missing\>|"
problem_pattern+="\<need\>|"
problem_pattern+="\<no.*such.*file\>|"
problem_pattern+="\<not.*found\>|"
problem_pattern+="\<not.*supported\>|"
problem_pattern+="\<too many\>|"
problem_pattern+="\<unable\>|"
problem_pattern+="\<unavailable\>|"
problem_pattern+="\<unexpected|"
problem_pattern+="\<unknown\>|"
problem_pattern+="\<urgent|"
problem_pattern+="\<warn\>|"
problem_pattern+="\<warning\>|"
problem_pattern+="\<wrong\>"
problem_pattern+=")"

# List of patterns used to exclude messages that are not problems
problem_exclude_pattern="("
problem_exclude_pattern+="\<launching .* with:"
problem_exclude_pattern+=")"

usage()
{
	cat <<EOT
Usage: $script_name [options]

Summary: Collect data about an installation of @PROJECT_NAME@.

Description: Run this script as root to obtain a markdown-formatted summary
  of the environment of the @PROJECT_NAME@ installation. The output of this script
  can be pasted directly into a github issue at the address below:

      $issue_url

Options:

 -h | --help    : show this usage summary.
 -v | --version : show program version.

EOT
}

version()
{
	cat <<EOT
$script_name version $script_version
EOT
}

die()
{
	local msg="$*"
	echo >&2 "ERROR: $script_name: $msg"
	exit 1
}

msg()
{
	local msg="$*"
	echo "$msg"
}

heading()
{
	local name="$*"
	echo -e "\n# $name\n"
}

subheading()
{
	local name="$*"
	echo -e "\n## $name\n"
}

separator()
{
	echo -e '\n---\n'
}

# Create an unfoldable section.
#
# Note: end_section() must be called to terminate the fold.
start_section()
{
	local title="$1"

	cat <<EOT
<details>
<summary>${title}</summary>
<p>

EOT
}

end_section()
{
	cat <<EOT

</p>
</details>
EOT
}

show_header()
{
	start_section "Show <tt>$script_name</tt> details"
}

show_footer()
{
	end_section
}

have_cmd()
{
	local cmd="$1"

	command -v "$cmd" &>/dev/null
	local ret=$?

	if [ $ret -eq 0 ]; then
		msg "Have \`$cmd\`"
	else
		msg "No \`$cmd\`"
	fi

	[ $ret -eq 0 ]
}

have_service()
{
	local service="$1"

	systemctl status "${service}" >/dev/null 2>&1
}

show_quoted_text()
{
	local language="$1"

	shift

	local text="$*"

	echo "\`\`\`${language}"
	echo "$text"
	echo "\`\`\`"
}

run_cmd_and_show_quoted_output()
{
	local language="$1"

	shift

	local cmd="$*"

	local output

	output=$(eval "$cmd" 2>&1)

	start_section "<tt>$cmd</tt>"
	show_quoted_text "${language}" "$output"
	end_section
}

show_runtime_configs()
{
	local title="Runtime config files"
	start_section "$title"

	heading "$title"

	local configs config

	configs=$($runtime --@PROJECT_TYPE@-show-default-config-paths)
	if [ $? -ne 0 ]; then
		version=$($runtime --version|tr '\n' ' ')
		die "failed to check config files - runtime is probably too old ($version)"
	fi

	subheading "Runtime default config files"

	show_quoted_text "" "$configs"

	# add in the standard defaults for good measure "just in case"
	configs+=" /etc/@PROJECT_TAG@/configuration.toml"
	configs+=" /usr/share/defaults/@PROJECT_TAG@/configuration.toml"
	configs+=" @CONFIG_PATH@"
	configs+=" @SYSCONFIG@"

	# create a unique list of config files
	configs=$(echo $configs|tr ' ' '\n'|sort -u)

	subheading "Runtime config file contents"

	for config in $configs; do
		if [ -e "$config" ]; then
			run_cmd_and_show_quoted_output "toml" "cat \"$config\""
		else
			msg "Config file \`$config\` not found"
		fi
	done

	separator

	end_section
}

find_system_journal_problems()
{
	local name="$1"
	local program="$2"

	# select by identifier
	local selector="-t"

	local data_source="system journal"

	local problems=$(journalctl -q -o cat -a "$selector" "$program" |\
		grep "time=" |\
		grep -i -E "$problem_pattern" |\
		grep -iv -E "$problem_exclude_pattern" |\
		tail -n ${PROBLEM_LIMIT})

	if [ -n "$problems" ]; then
		msg "Recent $name problems found in $data_source:"
		show_quoted_text "" "$problems"
	else
		msg "No recent $name problems found in $data_source."
	fi
}

show_containerd_shimv2_log_details()
{
	local title="Kata Containerd Shim v2"
	subheading "$title logs"

	start_section "$title"
	find_system_journal_problems "$name" "kata"
	end_section
}

show_runtime_log_details()
{
	local title="Runtime logs"

	subheading "$title"

	start_section "$title"
	find_system_journal_problems "runtime" "@RUNTIME_NAME@"
	end_section
}

show_throttler_log_details()
{
	local title="Throttler logs"
	subheading "$title"

	start_section "$title"
	find_system_journal_problems "throttler" "@PROJECT_TYPE@-ksm-throttler"
	end_section
}

show_log_details()
{
	local title="Logfiles"
	start_section "$title"

	heading "$title"

	show_runtime_log_details
	show_throttler_log_details
	show_containerd_shimv2_log_details

	separator

	end_section
}

show_package_versions()
{
	local title="Packages"

	start_section "$title"

	heading "$title"

	local pattern="("
	local project

	# CC 2.x, 3.0 and runv runtimes. They shouldn't be installed but let's
	# check anyway.
	pattern+="cc-oci-runtime"
	pattern+="|cc-runtime"
	pattern+="|runv"

	# core components
	for project in @PROJECT_TYPE@
	do
		pattern+="|${project}-runtime"
		pattern+="|${project}-ksm-throttler"
		pattern+="|${project}-containers-image"
	done

	# assets
	pattern+="|linux-container"

	# hypervisor name prefix
	pattern+="|qemu-"

	pattern+=")"

	if have_cmd "dpkg"; then
		run_cmd_and_show_quoted_output "" "dpkg -l|grep -E \"$pattern\""
	fi

	if have_cmd "rpm"; then
		run_cmd_and_show_quoted_output "" "rpm -qa|grep -E \"$pattern\""
	fi

	separator

	end_section
}

show_container_mgr_details()
{
	local title="Container manager details"
	start_section "$title"

	heading "$title"

	if have_cmd "docker" >/dev/null; then
		start_section "Docker"

		subheading "Docker"

		local -a cmds

		cmds+=("docker version")
		cmds+=("docker info")
		cmds+=("systemctl show docker")

		local cmd

		for cmd in "${cmds[@]}"
		do
			run_cmd_and_show_quoted_output "" "$cmd"
		done

		end_section
	fi

	if have_cmd "kubectl" >/dev/null; then
		title="Kubernetes"

		start_section "$title"

		subheading "$title"
		run_cmd_and_show_quoted_output "" "kubectl version"
		run_cmd_and_show_quoted_output "" "kubectl config view"

		local cmd="systemctl show kubelet"
		run_cmd_and_show_quoted_output "" "$cmd"

		end_section
	fi

	if have_cmd "crio" >/dev/null; then
		title="crio"
		start_section "$title"

		subheading "$title"

		run_cmd_and_show_quoted_output "" "crio --version"

		local cmd="systemctl show crio"
		run_cmd_and_show_quoted_output "" "$cmd"

		cmd="crio config"
		run_cmd_and_show_quoted_output "" "$cmd"

		end_section
	fi

	if have_cmd "containerd" >/dev/null; then
		title="containerd"
		start_section "$title"

		subheading "$title"

		run_cmd_and_show_quoted_output "" "containerd --version"

		local cmd="systemctl show containerd"
		run_cmd_and_show_quoted_output "" "$cmd"

		local file="/etc/containerd/config.toml"

		cmd="cat $file"
		run_cmd_and_show_quoted_output "toml" "$cmd"

		end_section
	fi

	if have_cmd "podman" >/dev/null; then
		title="Podman"

		start_section "$title"

		subheading "$title"
		run_cmd_and_show_quoted_output "" "podman --version"

		run_cmd_and_show_quoted_output "" "podman system info"

		local cmd file

		for file in {/etc,/usr/share}/containers/*.{conf,json}; do
			if [ -e "$file" ]; then
				cmd="cat $file"
				run_cmd_and_show_quoted_output "" "$cmd"
			fi
		done

		end_section
	fi

	separator

	end_section
}

show_meta()
{
	local date

	heading "Meta details"

	date=$(date '+%Y-%m-%d.%H:%M:%S.%N%z')
	msg "Running \`$script_name\` version \`$script_version\` at \`$date\`."

	separator
}

show_runtime()
{
	local cmd

	start_section "Runtime"

	msg "Runtime is \`$runtime\`."

	cmd="@PROJECT_TYPE@-env"

	heading "\`$cmd\`"

	run_cmd_and_show_quoted_output "toml" "$runtime $cmd"

	separator

	end_section
}

show_containerd_shimv2()
{
	start_section "Containerd shim v2"

	local cmd="${containerd_shim_v2} --version"

	msg "Containerd shim v2 is \`$containerd_shim_v2\`."

	run_cmd_and_show_quoted_output "" "$cmd"

	separator

	end_section
}

# Parameter 1: Path to disk image file.
# Returns: Details of the image, or "$unknown" on error.
get_image_details()
{
	local img="$1"

	[ -z "$img" ] && { echo "$unknown"; return;}
	[ -e "$img" ] || { echo "$unknown"; return;}

	local loop_device
	local partition_path
	local partitions
	local partition
	local count
	local mountpoint
	local contents
	local expected

	loop_device=$(loopmount_image "$img")
	if [ -z "$loop_device" ]; then
		echo "$unknown"
		return
	fi

	partitions=$(get_partitions "$loop_device")
	count=$(echo "$partitions"|wc -l)

	expected=1

	if [ "$count" -ne "$expected" ]; then
		release_device "$loop_device"
		echo "$unknown"
		return
	fi

	partition="$partitions"

	partition_path="/dev/${partition}"
	if [ ! -e "$partition_path" ]; then
		release_device "$loop_device"
		echo "$unknown"
		return
	fi

	mountpoint=$(mount_partition "$partition_path")

	contents=$(read_osbuilder_file "${mountpoint}")
	[ -z "$contents" ] && contents="$unknown"

	unmount_partition "$mountpoint"
	release_device "$loop_device"

	echo "$contents"
}

# Parameter 1: Path to the initrd file.
# Returns: Details of the initrd, or "$unknown" on error.
get_initrd_details()
{
	local initrd="$1"

	[ -z "$initrd" ] && { echo "$unknown"; return;}
	[ -e "$initrd" ] || { echo "$unknown"; return;}

	local file
	local relative_file=""
	local tmp

	file="${osbuilder_file}"

	# All files in the cpio archive are relative so remove leading slash
	relative_file=$(echo "$file"|sed 's!^/!!g')

	local tmpdir=$(mktemp -d)

	# Note: 'cpio --directory' seems to be non-portable, so cd(1) instead.
	(cd "$tmpdir" && gzip -dc "$initrd" | cpio \
		--extract \
		--make-directories \
		--no-absolute-filenames \
		$relative_file 2>/dev/null)

	contents=$(read_osbuilder_file "${tmpdir}")
	[ -z "$contents" ] && contents="$unknown"

	tmp="${tmpdir}/${file}"
	[ -d "$tmp" ] && rm -rf "$tmp"

	echo "$contents"
}

# Returns: Full path to the image file.
get_image_file()
{
	local cmd="@PROJECT_TYPE@-env"
	local cmdline="$runtime $cmd"

	local image=$(eval "$cmdline" 2>/dev/null |\
		grep -A 1 '^\[Image\]' |\
		grep -E "\<Path\> =" |\
		awk '{print $3}' |\
		tr -d '"')

	echo "$image"
}

# Returns: Full path to the initrd file.
get_initrd_file()
{
	local cmd="@PROJECT_TYPE@-env"
	local cmdline="$runtime $cmd"

	local initrd=$(eval "$cmdline" 2>/dev/null |\
		grep -A 1 '^\[Initrd\]' |\
		grep -E "\<Path\> =" |\
		awk '{print $3}' |\
		tr -d '"')

	echo "$initrd"
}

# Parameter 1: Path to disk image file.
# Returns: Path to loop device.
loopmount_image()
{
	local img="$1"
	[ -n "$img" ] || die "need image file"

	local device_path

	losetup -fP "$img"

	device_path=$(losetup -j "$img" |\
		cut -d: -f1 |\
		sort -k1,1 |\
		tail -1)

	echo "$device_path"
}

# Parameter 1: Path to loop device.
# Returns: Partition names.
get_partitions()
{
	local device_path="$1"
	[ -n "$device_path" ] || die "need device path"

	local device
	local partitions

	device=${device_path/\/dev\//}

	partitions=$(lsblk -nli -o NAME "${device_path}" |\
		grep -v "^${device}$")

	echo "$partitions"
}

# Parameter 1: Path to disk partition device.
# Returns: Mountpoint.
mount_partition()
{
	local partition="$1"
	[ -n "$partition" ] || die "need partition"
	[ -e "$partition" ] || die "partition does not exist: $partition"

	local mountpoint

	mountpoint=$(mktemp -d)

	mount -oro,noload "$partition" "$mountpoint"

	echo "$mountpoint"
}

# Parameter 1: Mountpoint.
unmount_partition()
{
	local mountpoint="$1"
	[ -n "$mountpoint" ] || die "need mountpoint"
	[ -e "$mountpoint" ] || die "mountpoint does not exist: $mountpoint"

	umount "$mountpoint"
}

# Parameter 1: Loop device path.
release_device()
{
	local device="$1"
	[ -n "$device" ] || die "need device"
	[ -e "$device" ] || die "device does not exist: $device"

	losetup -d "$device"
}

show_throttler_details()
{
	start_section "KSM throttler"

	heading "KSM throttler"

	subheading "version"

	local throttlers
	local throttler

	throttlers=$(find /usr/libexec /usr/lib* -type f |\
		grep -v trigger |\
		grep -E "(cc|kata)-ksm-throttler" |\
		sort -u)

	echo "$throttlers" | while read throttler
	do
		[ -z "$throttler" ] && continue

		local cmd
		cmd="$throttler --version"
		run_cmd_and_show_quoted_output "" "$cmd"
	done

	subheading "systemd service"

	local unit

	# Note: "vc-throttler" is the old CC service, replaced by
	# "kata-vc-throttler".
	for unit in \
		"cc-ksm-throttler" \
		"kata-ksm-throttler" \
		"kata-vc-throttler" \
		"vc-throttler"
	do
		have_service "${unit}" && \
			run_cmd_and_show_quoted_output "" "systemctl show ${unit}"
	done

	end_section
}

show_kata_monitor_version()
{
	start_section "Kata Monitor"

	local cmd="${kata_monitor} --version"

	msg "Kata Monitor \`$kata_monitor_name\`."

	run_cmd_and_show_quoted_output "" "$cmd"

	separator

	end_section
}

# Retrieve details of the image containing
# the rootfs used to boot the virtual machine.
show_image_details()
{
	local title="Image details"
	start_section "$title"

	heading "$title"

	local image
	local details

	image=$(get_image_file)

	if [ -n "$image" ]
	then
		details=$(get_image_details "$image")
		show_quoted_text "yaml" "$details"
	else
		msg "No image"
	fi

	separator

	end_section
}

# Retrieve details of the initrd containing
# the rootfs used to boot the virtual machine.
show_initrd_details()
{
	start_section "Initrd details"

	local initrd
	local details

	initrd=$(get_initrd_file)

	heading "Initrd details"

	if [ -n "$initrd" ]
	then
		details=$(get_initrd_details "$initrd")
		show_quoted_text "yaml" "$details"
	else
		msg "No initrd"
	fi

	separator

	end_section
}

read_osbuilder_file()
{
	local rootdir="$1"

	[ -n "$rootdir" ] || die "need root directory"

	local file="${rootdir}/${osbuilder_file}"

	[ ! -e "$file" ] && return

	cat "$file"
}

show_details()
{
	show_header

	show_meta
	show_runtime
	show_runtime_configs
	show_containerd_shimv2
	show_throttler_details
	show_image_details
	show_initrd_details
	show_log_details
	show_container_mgr_details
	show_package_versions
	show_kata_monitor_version

	show_footer
}

main()
{
	args=$(getopt \
		-n "$script_name" \
		-a \
		--options="dhv" \
		--longoptions="debug help version" \
		-- "$@")

	eval set -- "$args"
	[ $? -ne 0 ] && { usage && exit 1; }
	[ $# -eq 0 ] && { usage && exit 0; }

	while [ $# -gt 1 ]
	do
		case "$1" in
			-d|--debug)
				set -x
				;;

			-h|--help)
				usage && exit 0
				;;

			-v|--version)
				version && exit 0
				;;

			--)
				shift
				break
				;;
		esac
		shift
	done

	[ $(id -u) -eq 0 ] || die "Need to run as root"
	[ -n "$runtime" ] || die "cannot find runtime '$runtime_name'"

	show_details
}

main "$@"
